/* bzflag
 * Copyright (c) 1993-2020 Tim Riker
 *
 * This package is free software;  you can redistribute it and/or
 * modify it under the terms of the license found in the file
 * named COPYING that should have accompanied this file.
 *
 * THIS PACKAGE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
 */

/* interface header */
#include "commands.h"

/* system implementation headers */
#ifndef _WIN32
#  include <sys/types.h>
#  include <dirent.h>
#endif
#include <string>
#include <vector>
#include <cstdlib>
#include <png.h>
#include <zlib.h>

/* common implementation headers */
#include "BZDBCache.h"
#include "TextUtils.h"
#include "FileManager.h"
#include "DirectoryNames.h"
#include "version.h"
#include "SceneRenderer.h"
#include "bzglob.h"

/* local implementation headers */
#include "LocalPlayer.h"
#include "sound.h"
#include "ComposeDefaultKey.h"
#include "SilenceDefaultKey.h"
#include "ServerCommandKey.h"
#include "Roaming.h"
#include "playing.h"
#include "HUDRenderer.h"
#include "HUDui.h"

/** jump
 */
static std::string cmdJump(const std::string&,
                           const CommandManager::ArgList& args, bool*);

/** fire weapon
 */
static std::string cmdFire(const std::string&,
                           const CommandManager::ArgList& args, bool*);

/** drop a flag
 */
static std::string cmdDrop(const std::string&,
                           const CommandManager::ArgList& args, bool*);

/** toggle radar visibility
 */
static std::string cmdToggleRadar(const std::string&,
                                  const CommandManager::ArgList& args, bool*);

/** toggle console visibility
 */
static std::string cmdToggleConsole(const std::string&,
                                    const CommandManager::ArgList& args, bool*);

/** toggle flags
 */
static std::string cmdToggleFlags(const std::string&,
                                  const CommandManager::ArgList& args, bool*);

/** identify to a server
 */
static std::string cmdIdentify(const std::string&,
                               const CommandManager::ArgList& args, bool*);

/** restart/respawn
 */
static std::string cmdRestart(const std::string&,
                              const CommandManager::ArgList& args, bool*);

/** self-destruct
 */
static std::string cmdDestruct(const std::string&,
                               const CommandManager::ArgList& args, bool*);

/** pause
 */
static std::string cmdPause(const std::string&,
                            const CommandManager::ArgList& args, bool*);

/** select tabbed message
 */
static std::string cmdMessagePanel(const std::string&,
                                   const CommandManager::ArgList& args, bool*);

/** toggle auto-pilot
 */
static std::string cmdAutoPilot(const std::string&,
                                const CommandManager::ArgList& args, bool*);

/** change radar scale
 */
static std::string cmdRadarZoom(const std::string&,
                                const CommandManager::ArgList& args, bool*);

/** change view angle
 */
static std::string cmdViewZoom(const std::string&,
                               const CommandManager::ArgList& args, bool*);

/** send
 */
static std::string cmdSend(const std::string&,
                           const CommandManager::ArgList& args, bool*);

/** capture a screenshot
 */
static std::string cmdScreenshot(const std::string&,
                                 const CommandManager::ArgList& args, bool*);

/** time
 */
static std::string cmdTime(const std::string&,
                           const CommandManager::ArgList& args, bool*);

/** roam
 */
static std::string cmdRoam(const std::string&,
                           const CommandManager::ArgList& args, bool*);

/** silence another player
 */
static std::string cmdSilence(const std::string&,
                              const CommandManager::ArgList& args, bool*);

/** perform a server command
 */
static std::string cmdServerCommand(const std::string&,
                                    const CommandManager::ArgList& args,
                                    bool*);

/** scroll the chat panel
 */
static std::string cmdScrollPanel(const std::string&,
                                  const CommandManager::ArgList& args, bool*);

/** hunt a player
 */
static std::string cmdHunt(const std::string&,
                           const CommandManager::ArgList& args, bool*);

/** hunt another player
 */
static std::string cmdAddHunt(const std::string&,
                              const CommandManager::ArgList& args, bool*);

/** iconify window
 */
static std::string cmdIconify(const std::string&,
                              const CommandManager::ArgList& args, bool*);

/** mouse box size flags
 */
static std::string cmdMouseBox(const std::string&,
                               const CommandManager::ArgList& args, bool*);

/** toggle mouse capture
 */
static std::string cmdMouseGrab(const std::string&,
                                const CommandManager::ArgList& args, bool*);

/** toggle Full Screen
 */
static std::string cmdToggleFS(const std::string&,
                               const CommandManager::ArgList& args, bool*);

/** cycle to the next radar zoom level
 */
static std::string cmdCycleRadar(const std::string&,
                                 const CommandManager::ArgList& args, bool*);

/** cycle to the next panel tab
 */
static std::string cmdCyclePanel(const std::string&,
                                 const CommandManager::ArgList& args, bool*);


const struct CommandListItem commandList[] =
{
    { "fire",     &cmdFire,       "fire:  fire a shot" },
    { "jump",     &cmdJump,       "jump:  make player jump" },
    { "drop",     &cmdDrop,       "drop:  drop the current flag" },
    { "identify",     &cmdIdentify,       "identify:  identify/lock-on-to player in view" },
    { "restart",      &cmdRestart,        "restart:  restart playing" },
    { "destruct",     &cmdDestruct,       "destruct:  self destruct" },
    { "pause",        &cmdPause,      "pause:  pause/resume" },
    { "send",     &cmdSend,       "send {all|team|nemesis|recipient|admin}:  start composing a message" },
    { "screenshot",   &cmdScreenshot,     "screenshot:  take a screenshot" },
    { "time",     &cmdTime,       "time {forward|backward}:  adjust the current time" },
    { "roam",     &cmdRoam,       "roam {zoom|cycle} <args>:  roam around" },
    { "silence",      &cmdSilence,        "silence:  silence/unsilence a player" },
    { "servercommand",    &cmdServerCommand,  "servercommand:  quick admin" },
    { "scrollpanel",  &cmdScrollPanel,    "scrollpanel {up|down}:  scroll message panel" },
    { "hunt",     &cmdHunt,       "hunt:  hunt a specific player" },
    { "addhunt",      &cmdAddHunt,        "addhunt:  add/modify hunted player(s)" },
    { "iconify",      &cmdIconify,        "iconify: iconify & pause bzflag" },
    { "mousebox",     &cmdMouseBox,       "mousebox <size>:  change the mousebox size"},
    { "mousegrab",    &cmdMouseGrab,      "mousegrab: toggle exclusive mouse mode" },
    { "fullscreen",   &cmdToggleFS,       "fullscreen: toggle fullscreen mode" },
    { "autopilot",    &cmdAutoPilot,      "autopilot:  set/unset autopilot bot code" },
    { "radarZoom",    &cmdRadarZoom,      "radarZoom {in/out}: change maxRadar range"},
    { "viewZoom",     &cmdViewZoom,       "viewZoom {in/out/toggle}: change view angle" },
    { "messagepanel", &cmdMessagePanel,   "messagepanel {all|chat|server|misc}:  set message tab" },
    { "toggleRadar",  &cmdToggleRadar,    "toggleRadar:  toggle radar visibility" },
    { "toggleConsole",    &cmdToggleConsole,  "toggleConsole:  toggle console visibility" },
    { "toggleFlags",  &cmdToggleFlags,    "toggleFlags {main|radar}:  turn off/on field radar flags" },
    { "cycleRadar",   &cmdCycleRadar,     "cycleRadar {level1 [level2 ...] [off]}:  cycle to the next radar zoom level" },
    { "cyclePanel",   &cmdCyclePanel,     "cyclePanel {left[_off]|right[_off]}:  cycle to the previous or next message panel tab" }
};


static std::string cmdToggleFS(const std::string&,
                               const CommandManager::ArgList& args, bool*)
{
    if (args.size() != 0)
        return "usage: fullscreen";
    mainWindow->toggleFullscreen();
    mainWindow->getWindow()->callResizeCallbacks();
    return std::string();
}

static std::string cmdMouseBox(const std::string&,
                               const CommandManager::ArgList& args, bool*)
{
    if (args.size() != 1)
        return "usage: mousebox <size>";
    const char* start = args[0].c_str();
    char* end;
    const int value = (int) strtol(args[0].c_str(), &end, 10);
    if (start == end)
        return "bad number";
    RENDERER.setMaxMotionFactor(value);
    return std::string();
}

static std::string cmdMouseGrab(const std::string&,
                                const CommandManager::ArgList& args, bool*)
{
    if (args.size() != 0)
        return "usage: mousegrab";
    const bool grabbing = !(BZDB.isTrue("mousegrab"));
    BZDB.set("mousegrab", grabbing ? "true" : "false");
    mainWindow->enableGrabMouse(grabbing);
    return std::string();
}

static std::string cmdIconify(const std::string&,
                              const CommandManager::ArgList& args, bool*)
{
    if (args.size() != 0)
        return "usage: iconify";

    if (!BZDB.isTrue("Win32NoMin"))
        mainWindow->iconify();
    return std::string();
}

static std::string cmdJump(const std::string&,
                           const CommandManager::ArgList& args, bool*)
{
    if (args.size() != 0)
        return "usage: jump";
    LocalPlayer *myTank = LocalPlayer::getMyTank();
    if (myTank != NULL)
        myTank->setJump();
    return std::string();
}

static std::string cmdToggleFlags(const std::string&, const
                                  CommandManager::ArgList& args, bool*)
{
    if (args.size() != 1)
        return "usage: main|radar";
    if (args[0] == "main")
    {
        CMDMGR.run("toggle displayMainFlags");
        warnAboutMainFlags();
    }
    else if (args[0] == "radar")
    {
        CMDMGR.run("toggle displayRadarFlags");
        warnAboutRadarFlags();
    }
    else
        return "usage: main|radar";

    return std::string();
}

static std::string cmdToggleRadar(const std::string&,
                                  const CommandManager::ArgList& args, bool*)
{
    if (args.size() != 0)
        return "usage: toggleRadar";

    CMDMGR.run("toggle displayRadar");

    warnAboutRadar();

    return std::string();
}

static std::string cmdToggleConsole(const std::string&,
                                    const CommandManager::ArgList& args, bool*)
{
    if (args.size() != 0)
        return "usage: toggleConsole";

    CMDMGR.run("toggle displayConsole");

    warnAboutConsole();

    return std::string();
}


static std::string cmdFire(const std::string&,
                           const CommandManager::ArgList& args, bool*)
{
    if (args.size() != 0)
        return "usage: fire";
    LocalPlayer *myTank = LocalPlayer::getMyTank();
    if (fireButton && myTank != NULL && myTank->isAlive()
            && myTank->getTeam() != ObserverTeam)
        myTank->fireShot();
    return std::string();
}

static std::string cmdDrop(const std::string&,
                           const CommandManager::ArgList& args, bool*)
{
    if (args.size() != 0)
        return "usage: drop";
    LocalPlayer *myTank = LocalPlayer::getMyTank();
    if (myTank != NULL)
    {
        FlagType::Ptr flag = myTank->getFlag();
        if ((flag != Flags::Null) && !myTank->isPaused() &&
                (flag->endurance != FlagEndurance::Sticky) && !myTank->isPhantomZoned() &&
                !(flag->flagEffect == FlagEffect::OscillationOverthruster &&
                  myTank->getLocation() == LocalPlayer::InBuilding))
        {
            serverLink->sendDropFlag(myTank->getPosition());
            // changed: on windows it may happen the MsgDropFlag
            // never comes back to us, so we drop it right away
            handleFlagDropped(myTank);
        }
    }
    return std::string();
}

static std::string cmdIdentify(const std::string&,
                               const CommandManager::ArgList& args, bool*)
{
    if (args.size() != 0)
        return "usage: identify";
    LocalPlayer *myTank = LocalPlayer::getMyTank();

    // per specification from blast007, GM locks can only be initizated by people with the GM flag
    if (myTank != nullptr && myTank->isAlive() && !myTank->isPaused()
            && myTank->getFlag()->flagEffect == FlagEffect::GuidedMissile)
        setTarget();

    return std::string();
}

static std::string cmdRestart(const std::string&,
                              const CommandManager::ArgList& args, bool*)
{
    if (args.size() != 0)
        return "usage: restart";
    LocalPlayer *myTank = LocalPlayer::getMyTank();
    if (myTank != NULL)
        if (!gameOver && !myTank->isSpawning() && (myTank->getTeam() != ObserverTeam) && !myTank->isAlive()
                && !myTank->isExploding())
        {
            serverLink->sendAlive();
            myTank->setSpawning(true);
            CommandManager::ArgList zoomArgs;
            std::string resetArg = "reset";
            zoomArgs.push_back(resetArg);
            cmdViewZoom("", zoomArgs,NULL);
        }

    return std::string();
}

static std::string cmdDestruct(const std::string&,
                               const CommandManager::ArgList& args, bool*)
{
    if (args.size() != 0)
        return "usage: destruct";
    LocalPlayer *myTank = LocalPlayer::getMyTank();
    if (myTank != NULL)
    {
        if (destructCountdown > 0.0f)
        {
            destructCountdown = 0.0f;
            hud->setAlert(1, "Self Destruct cancelled", 1.5f, true);
        }
        else
        {
            destructCountdown = 5.0f;
            char msgBuf[40];
            sprintf(msgBuf, "Self Destructing in %d", (int)(destructCountdown + 0.99f));
            hud->setAlert(1, msgBuf, 1.0f, false);
        }
    }
    return std::string();
}

static std::string cmdPause(const std::string&,
                            const CommandManager::ArgList& args, bool*)
{
    if (args.size() != 0)
        return "usage: pause";

    LocalPlayer *myTank = LocalPlayer::getMyTank();
    if (!pausedByUnmap && myTank && myTank->isAlive() && !myTank->isAutoPilot())
    {
        if (myTank->isPaused())
        {
            // already paused, so unpause
            myTank->setPause(false);
            controlPanel->addMessage("Resumed");

            // restore the sound
            if (savedVolume != -1)
            {
                setSoundVolume(savedVolume);
                savedVolume = -1;
            }

            // grab mouse
            if (shouldGrabMouse())
                mainWindow->grabMouse();

        }
        else if (pauseCountdown > 0.0f)
        {
            // player aborted pause
            pauseCountdown = 0.0f;
            hud->setAlert(1, "Pause cancelled", 1.5f, true);

        }
        else if (myTank->getLocation() == LocalPlayer::InBuilding)
        {
            // custom message when trying to pause while in a building
            // (could get stuck on un-pause if flag is taken)
            hud->setAlert(1, "Can't pause while inside a building", 1.0f, false);

        }
        else if (myTank->getLocation() == LocalPlayer::InAir)
        {
            // custom message when trying to pause when jumping/falling
            hud->setAlert(1, "Can't pause when you are in the air", 1.0f, false);

        }
        else if (myTank->getLocation() != LocalPlayer::OnGround &&
                 myTank->getLocation() != LocalPlayer::OnBuilding)
        {
            // catch-all message when trying to pause when you should not
            hud->setAlert(1, "Unable to pause right now", 1.0f, false);

        }
        else
        {
            // update the pause alert message
            pauseCountdown = 5.0f;
            char msgBuf[40];
            sprintf(msgBuf, "Pausing in %d", (int) (pauseCountdown + 0.99f));
            hud->setAlert(1, msgBuf, 1.0f, false);
        }
    }
    return std::string();
}

static std::string cmdAutoPilot(const std::string&,
                                const CommandManager::ArgList& args, bool*)
{
    if (args.size() != 0)
        return "usage: autopilot";

    // don't enable autopilot until we've fully joined and checked the value
    // of the server-side _disableBots
    if (! BZDB.isSet(StateDatabase::BZDB_DISABLEBOTS))
        return std::string();

    LocalPlayer *myTank = LocalPlayer::getMyTank();

    if (!BZDB.isTrue(StateDatabase::BZDB_TANKWIDTH))
        return std::string();

    if ((myTank == NULL) || (myTank->getTeam() == ObserverTeam))
        return std::string();

    if (myTank->isAutoPilot())
    {

        myTank->activateAutoPilot(false);
        hud->setAlert(0, "autopilot disabled", 1.0f, true);

        // grab mouse
        if (shouldGrabMouse()) mainWindow->grabMouse();

    }
    else if (BZDB.isTrue(StateDatabase::BZDB_DISABLEBOTS))
        hud->setAlert(0, "autopilot not allowed on this server", 1.0f, true);
    else
    {

        // don't enable the AutoPilot if you have within the last 5 secs
        static TimeKeeper LastAutoPilotEnable = TimeKeeper::getSunGenesisTime();
        if ((TimeKeeper::getCurrent() - LastAutoPilotEnable) > 5)
        {
            // reset timer
            LastAutoPilotEnable = TimeKeeper::getCurrent();

            // enable autopilot
            myTank->activateAutoPilot();
            hud->setAlert(0, "autopilot enabled", 1.0f, true);

            // ungrab mouse
            mainWindow->ungrabMouse();
        }
        else
            controlPanel->addMessage("You may not enable the Autopilot more than once every five seconds.");

    }

    return std::string();
}

static std::string cmdRadarZoom(const std::string&,
                                const CommandManager::ArgList& args, bool*)
{
    if (args.size() != 1)
        return "usage: radarZoom {in|out}";

    float range = BZDB.eval("displayRadarRange");

    if (args[0] == "out")
    {
        range *= 1.05f;
        if (range > 2.0f)
            range = 2.0f;
        BZDB.setFloat("displayRadarRange", range);
    }
    else if (args[0] == "in")
    {
        range /= 1.05f;
        if (range < 0.005f)
            range = 0.005f;
        BZDB.setFloat("displayRadarRange", range);
    }
    else
        return "usage: radarZoom {in|out}";

    return std::string();
}

static std::string cmdViewZoom(const std::string&,
                               const CommandManager::ArgList& args, bool*)
{
    if (args.size() != 1)
        return "usage: viewZoom {in|out|toggle}";

    float fov = BZDB.eval("displayFOV");
    float defFov = BZDB.eval("defaultFOV");

    if (args[0] == "out")
    {
        fov += 1.0f;
        if (fov > defFov)
            fov = defFov;
        BZDB.setFloat("displayFOV", fov);
    }
    else if (args[0] == "in")
    {
        fov -= 1.0f;
        if (fov < 15.0f)
            fov = 15.0f;
        BZDB.setFloat("displayFOV", fov);
    }
    else if (args[0] == "toggle")
    {
        if (fov < 15.5f)
            fov = defFov;
        else
            fov = 15.0f;
        BZDB.setFloat("displayFOV", fov);
        // also toggle the observer fov
        if (ROAM.getZoom() != defFov)
            ROAM.setZoom(defFov);
        else
            ROAM.setZoom(15.0f);
    }
    else if (args[0] == "reset")
    {
        fov = defFov;
        ROAM.setZoom(defFov);
        BZDB.setFloat("displayFOV", fov);
    }
    else
        return "usage: viewZoom {in|out|toggle|reset}";

    return std::string();
}

static std::string cmdMessagePanel(const std::string&,
                                   const CommandManager::ArgList& args, bool*)
{
    if (args.size() != 1)
        return "usage: messagepanel {all|chat|server|misc}";

    int oldMode = controlPanel->getMessagesMode();
    int newMode;
    if (args[0] == "all")
        newMode = 0;
    else if (args[0] == "chat")
        newMode = 1;
    else if (args[0] == "server")
        newMode = 2;
    else if (args[0] == "misc")
        newMode = 3;
    else
        return "usage: messagepanel {all|chat|server|misc}";

    if (newMode == oldMode)
        newMode = -1;
    controlPanel->setMessagesMode(newMode);

    return std::string();
}

static std::string cmdSend(const std::string&,
                           const CommandManager::ArgList& args, bool*)
{
    static ComposeDefaultKey composeKeyHandler;
    if (args.size() != 1)
        return "usage: send {all|team|nemesis|recipient|admin}";
    LocalPlayer *myTank = LocalPlayer::getMyTank();
    if (!myTank)
        return "use send only when connected";
    std::string composePrompt;
    if (args[0] == "all")
    {
        void* buf = messageMessage;
        buf = nboPackUByte(buf, AllPlayers);
        composePrompt = "Send to all: ";
    }
    else if (args[0] == "team")
    {
        if (World::getWorld()->allowTeams() || myTank->getTeam() == ObserverTeam)
        {
            void* buf = messageMessage;
            buf = nboPackUByte(buf, TeamToPlayerId(myTank->getTeam()));
            composePrompt = "Send to teammates: ";
        }
        else
        {
            void* buf = messageMessage;
            buf = nboPackUByte(buf, AllPlayers);
            composePrompt = "Send to all: ";
        }
    }
    else if (args[0] == "nemesis")
    {
        const Player* nemesis = myTank->getNemesis();
        if (!nemesis) return std::string();

        void* buf = messageMessage;
        buf = nboPackUByte(buf, nemesis->getId());
        composePrompt = "Send to ";
        composePrompt += nemesis->getCallSign();
        composePrompt += ": ";
    }
    else if (args[0] == "recipient")
    {
        const Player* recipient = myTank->getRecipient();
        if (!recipient)
        {
            for (int i = 0; i < curMaxPlayers; i++)
            {
                if (remotePlayers[i])
                {
                    myTank->setRecipient(remotePlayers[i]);
                    break;
                }
            }
        }
        recipient = myTank->getRecipient();
        if (recipient)
        {
            void* buf = messageMessage;
            buf = nboPackUByte(buf, recipient->getId());
            composePrompt = "Send to ";
            composePrompt += recipient->getCallSign();
            composePrompt += ": ";
        }
    }
    else if (args[0] == "admin")
    {
        void* buf = messageMessage;
        buf = nboPackUByte(buf, AdminPlayers);
        composePrompt = "Send to Admin: ";

    }
    else
        return "usage: send {all|team|nemesis|recipient|admin}";
    messageHistoryIndex = 0;
    hud->setComposing(composePrompt);
    HUDui::setDefaultKey(&composeKeyHandler);
    return std::string();
}


struct ScreenshotData
{
    std::string renderer;
    unsigned char* pixels;
    unsigned int width;
    unsigned int height;
};


static void* writeScreenshot(void* data)
{
    ScreenshotData* ssdata = (ScreenshotData*)data;

    const std::string dirname = getScreenShotDirName();
    const std::string prefix  = "bzfi";
    const std::string ext     = ".png";

    // scan the directory and start numbering with the filename
    // that follows the existing filename with the highest snap number
    int snap = 0;

#ifdef _WIN32
    const std::string pattern = dirname + prefix + "*" + ext;
    WIN32_FIND_DATA findData;
    HANDLE h = FindFirstFile(pattern.c_str(), &findData);
    if (h != INVALID_HANDLE_VALUE)
    {
        std::string file = findData.cFileName;
        snap = atoi((file.substr(file.length() - 8, 4)).c_str());
        while (FindNextFile(h, &findData))
        {
            file = findData.cFileName;
            const int number = atoi((file.substr(file.length() - 8, 4)).c_str());
            if (snap < number)
                snap = number;
        }
    }
#else
    const std::string pattern = prefix + "*" + ext;
    DIR* directory = opendir(dirname.c_str());
    if (directory)
    {
        struct dirent* contents;
        std::string file;
        while ((contents = readdir(directory)))
        {
            file = contents->d_name;
            if (glob_match(pattern, file))
            {
                const int number = atoi((file.substr(file.length() - 8, 4)).c_str());
                if (snap < number)
                    snap = number;
            }
        }
        closedir(directory);
    }
#endif // _WIN32

    snap++;
    std::string filename = dirname + prefix + TextUtils::format("%04d", snap) + ext;

    std::ostream* output = FILEMGR.createDataOutStream(filename.c_str(), true, true);

    if (output != nullptr)
    {
        const std::string& renderer = ssdata->renderer;
        unsigned char* pixels       = ssdata->pixels;
        const unsigned int width         = ssdata->width;
        const unsigned int height        = ssdata->height;

        // Gamma-correction is preapplied by BZFlag's gamma table
        // This ignores the PNG gAMA chunk, but so do many viewers (including Mozilla)
        if (BZDB.isSet("gamma"))
        {
            const float gamma = BZDB.eval("gamma");
            if (gamma != 1.0f)
            {
                unsigned char gammaTable[256];
                for (int i = 0; i < 256; i++)
                {
                    const float lum    = float(i) / 256.0f;
                    const float lumadj = pow(lum, 1.0f / gamma);
                    gammaTable[i] = (unsigned char) (lumadj * 256);
                }
                const int pixelCount = (width * height * 3);
                for (int i = 0; i < pixelCount; i++)
                    pixels[i] = gammaTable[pixels[i]];
            }
        }

        // PNG structures
        png_structp png;
        png_infop pnginfo;

        // Create write structure
        png = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
        if (png == nullptr)
        {
            ControlPanel::addMutexMessage("Failed to create libpng write structure.");
            return nullptr;
        }

        // Create info structure
        pnginfo = png_create_info_struct(png);
        if (pnginfo == nullptr)
        {
            ControlPanel::addMutexMessage("Failed to create libpng info structure.");
            png_destroy_write_struct(&png, (png_infopp)0);
            return nullptr;
        }

        // Basic error handling
        if (setjmp(png_jmpbuf(png)))
        {
            ControlPanel::addMutexMessage("Error writing screenshot.");
            if (png != nullptr)
            {
                if (pnginfo != nullptr)
                    png_destroy_write_struct(&png, &pnginfo);
                else
                    png_destroy_write_struct(&png, (png_infopp)0);
            }

            return nullptr;
        }

        // Register function to send writes to the output stream
        png_set_write_fn(png, output,
                         [](png_structp _png, png_bytep _data, png_size_t _length) -> void
        {
            std::ostream* _output = (std::ostream*)(png_get_io_ptr(_png));
            try
            {
                _output->write((char*)_data, _length);
            }
            catch (std::ios_base::failure &e)
            {
                logDebugMessage(0, e.what());
                png_error(_png, "Error writing screenshot.");
            }
        },
        nullptr
                        );

        // Use RLE compression
        png_set_compression_strategy(png, Z_RLE);

        // Set header information
        png_set_IHDR(png, pnginfo, width, height, 8, PNG_COLOR_TYPE_RGB, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT,
                     PNG_FILTER_TYPE_DEFAULT);

        // Set Software text chunk
        const std::string versionStr = std::string("BZFlag ") + getAppVersion();
        png_text software_text;
        software_text.compression = PNG_TEXT_COMPRESSION_NONE;
        software_text.key = const_cast<png_charp>("Software");
        software_text.text = const_cast<png_charp>(reinterpret_cast<const char *>(versionStr.c_str()));
        png_set_text(png, pnginfo, &software_text, 1);

        // Set OpenGL Renderer text chunk
        png_text renderer_text;
        renderer_text.compression = PNG_TEXT_COMPRESSION_NONE;
        renderer_text.key = const_cast<png_charp>("OpenGL Renderer");
        renderer_text.text = const_cast<png_charp>(reinterpret_cast<const char *>(renderer.c_str()));
        png_set_text(png, pnginfo, &renderer_text, 1);

        // Write PNG header
        png_write_info(png, pnginfo);

        // Write each row of pixels
        for (size_t y = 0; y < height; y++)
            png_write_row(png, (png_bytep)(pixels + (height - y - 1) * width * 3));

        // Write the end of the file
        png_write_end(png, pnginfo);

        // Destroy libpng structures
        png_destroy_write_struct(&png, &pnginfo);

        // Close output stream
        delete output;

        // Tell the user about the location and dimensions of the screenshot
        char buf[128];
        snprintf(buf, sizeof(buf), "%s: %dx%d", filename.c_str(), width, height);
        ControlPanel::addMutexMessage(buf);
    }
    else
    {
        char buf[128];
        snprintf(buf, sizeof(buf), "%s: failed to save", filename.c_str());
        ControlPanel::addMutexMessage(buf);
    }

    // Free up memory
    delete[] ssdata->pixels;
    delete ssdata;

    return nullptr;
}

#ifdef _WIN32
static DWORD WINAPI writeWinScreenshot(void* data)
{
    return writeScreenshot(data) == nullptr ? 0 : 1;
}
#endif

static std::string cmdScreenshot(const std::string&, const CommandManager::ArgList& args, bool*)
{
    if (args.size() != 0)
        return "usage: screenshot";

    ScreenshotData* ssdata = new ScreenshotData;
    ssdata->renderer += (const char*)(glGetString(GL_VENDOR));
    ssdata->renderer += ": ";
    ssdata->renderer += (const char*)(glGetString(GL_RENDERER));
    ssdata->renderer += " (OpenGL ";
    ssdata->renderer += (const char*)(glGetString(GL_VERSION));
    ssdata->renderer += ")";
    int w = ssdata->width = mainWindow->getWidth();
    int h = ssdata->height = mainWindow->getHeight();
    ssdata->pixels = new unsigned char[h * w * 3];
    glPushClientAttrib(GL_CLIENT_PIXEL_STORE_BIT);
    glPixelStorei(GL_PACK_ALIGNMENT, 1);
    glReadPixels(0, 0, w, h, GL_RGB, GL_UNSIGNED_BYTE, ssdata->pixels);
    glPopClientAttrib();

#if defined(HAVE_PTHREADS)
    pthread_t thread;
    pthread_attr_t attr;

    pthread_attr_init(&attr);
    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
    pthread_create(&thread, &attr, writeScreenshot, (void *) ssdata);
    pthread_attr_destroy(&attr);
#elif defined(_WIN32)
    CreateThread(
        NULL, // Security attributes
        0, // Stack size (0 -> default)
        writeWinScreenshot,
        ssdata,
        0, // creation flags (0 -> run immediately)
        NULL); // thread id return value (NULL -> don't care)
#else
    // no threads?  sucks to be you, but we'll still write the screenshot
    writeScreenshot(ssdata);
#endif

    return std::string();
}

static std::string cmdTime(const std::string&,
                           const CommandManager::ArgList& args, bool*)
{
    // FIXME - time should be moved into BZDB
    if (args.size() != 1)
        return "usage: time {forward|backward}";
    if (args[0] == "forward")
        clockAdjust += 5.0f * 60.0f;
    else if (args[0] == "backward")
        clockAdjust -= 5.0f * 60.0f;
    else
        return "usage: time {forward|backward}";
    return std::string();
}

static std::string cmdRoam(const std::string&,
                           const CommandManager::ArgList& args, bool*)
{
    if (args.size() == 0)
        return "usage: roam {zoom|cycle} <args>";
    if (!ROAM.isRoaming())
        return std::string();
    if (args[0] == "zoom")
    {
        if (args.size() != 2)
            return "usage: roam zoom {in|out|normal|stop}";
        if (!roamButton || args[1] == "stop")
            roamDZoom = 0.0f;
        else if (args[1] == "out")
            roamDZoom = +2.0f * BZDBCache::tankSpeed;
        else if (args[1] == "in")
            roamDZoom = -2.0f * BZDBCache::tankSpeed;
        else if (args[1] == "normal")
            ROAM.setZoom(60.0f);
        else
            return "usage: roam zoom {in|out|normal|stop}";
    }
    else if (args[0] == "cycle")
    {
        if (args.size() != 3)
            return "usage: roam cycle {type|subject} {forward|backward}";
        if (args[1] == "type")
        {
            if (args[2] == "forward")
                ROAM.setMode(Roaming::RoamingView((ROAM.getMode() + 1) % Roaming::roamViewCount));
            else if (args[2] == "backward")
            {
                int setto = (ROAM.getMode() - 1) % Roaming::roamViewCount;
                if (setto < 0) setto += Roaming::roamViewCount;
                ROAM.setMode(Roaming::RoamingView(setto));
            }
            else
                return "usage: roam cycle {type|subject} {forward|backward}";
        }
        else if (args[1] == "subject")
        {
            if (args[2] == "forward")
                ROAM.changeTarget(Roaming::next);
            else if (args[2] == "backward")
                ROAM.changeTarget(Roaming::previous);
            else
                return "usage: roam cycle {type|subject} {forward|backward}";
        }
        else
            return "usage: roam cycle {type|subject} {forward|backward}";
    }
    else
        return "usage: roam {zoom|cycle} <args>";
    return std::string();
}

static std::string cmdSilence(const std::string&,
                              const CommandManager::ArgList& args, bool*)
{
    static SilenceDefaultKey silenceKeyHandler;
    if (args.size() != 0)
        return "usage: silence";
    messageHistoryIndex = 0;
    hud->setComposing("[Un]Silence: ");
    HUDui::setDefaultKey(&silenceKeyHandler);
    return std::string();
}

static std::string cmdServerCommand(const std::string&,
                                    const CommandManager::ArgList& args, bool*)
{
    static ServerCommandKey serverCommandKeyHandler;
    if (args.size() != 0)
        return "usage: servercommand";
    LocalPlayer *myTank = LocalPlayer::getMyTank();
    if (!myTank)
        return "use only when connected";
    static bool prevAdmin = myTank->isAdmin();
    if (!prevAdmin && myTank->isAdmin()) serverCommandKeyHandler.adminInit();
    if (prevAdmin && !myTank->isAdmin()) serverCommandKeyHandler.nonAdminInit();
    prevAdmin = myTank->isAdmin();

    messageHistoryIndex = 0;
    serverCommandKeyHandler.init();
    HUDui::setDefaultKey(&serverCommandKeyHandler);
    return std::string();
}

static std::string cmdScrollPanel(const std::string&,
                                  const CommandManager::ArgList& args, bool*)
{
    if ((args.size() < 1) || (args.size() > 2))
        return "usage: scrollpanel {up|up_page|down|down_page|top|bottom} [count]\n";
    int count = 1;
    int linecount = 2;
    if (args.size() == 2)
    {
        count = atoi(args[1].c_str());
        linecount = count;
    }
    // whence - (0 = set, 1 = cur, 2 = end)
    if (args[0] == "up")
        controlPanel->setMessagesOffset(+linecount, 1 /* current */, false);
    else if (args[0] == "down")
        controlPanel->setMessagesOffset(-linecount, 1 /* current */, false);
    else if (args[0] == "up_page")
        controlPanel->setMessagesOffset(+count, 1 /* current */, true);
    else if (args[0] == "down_page")
        controlPanel->setMessagesOffset(-count, 1 /* current */, true);
    else if (args[0] == "top")
        controlPanel->setMessagesOffset(123456789, 0 /* set */, false);
    else if (args[0] == "bottom")
        controlPanel->setMessagesOffset(0, 0 /* set */, false);
    else if (args[0] == "pause")
        controlPanel->togglePaused();
    return std::string();
}


static std::string cmdHunt(const std::string&,
                           const CommandManager::ArgList& args, bool*)
{
    if (args.size() != 0)
        return "usage: hunt";
    LocalPlayer *myTank = LocalPlayer::getMyTank();
    if (!myTank)
        return "use only when connected";
    hud->getScoreboard()->huntKeyEvent (false);
    return std::string();
}

static std::string cmdAddHunt(const std::string&,
                              const CommandManager::ArgList& args, bool*)
{
    if (args.size() != 0)
        return "usage: addhunt";
    LocalPlayer *myTank = LocalPlayer::getMyTank();
    if (!myTank)
        return "use only when connected";
    hud->getScoreboard()->huntKeyEvent (true);
    return std::string();
}

static std::string cmdCycleRadar(const std::string&,
                                 const CommandManager::ArgList& args, bool*)
{
    const std::string usageText = "usage: cycleRadar {level1 [level2 ...] [off]}:  cycle to the next radar zoom level";

    if (args.size() == 0)
        return usageText;

    std::vector<float> radarLevels;

    for (size_t i = 0; i < args.size(); ++i)
        if (args[i] == "off")
            radarLevels.push_back(0.0f);
        else if (atof(args[i].c_str()) > 0.0f)
            radarLevels.push_back((float)atof(args[i].c_str()));
        else
            return usageText;

    if (radarLevels.size() == 0)
        return usageText;

    if (radarLevels.size() == 1)
    {
        // only one specified... just set it
        BZDB.set("displayRadar", radarLevels[0] > 0.0f ? "1" : "0");

        if (radarLevels[0] > 0.0f)
            BZDB.setFloat("displayRadarRange", radarLevels[0]);

        return std::string();
    }

    static size_t radarLevelIndex = radarLevels.size() - 1;

    // if it's off and the current level is some form of on, turn it back on and set it
    if (BZDB.get("displayRadar") == "0" && radarLevels[radarLevelIndex] > 0.0f)
    {
        BZDB.set("displayRadar", "1");
        BZDB.setFloat("displayRadarRange", radarLevels[radarLevelIndex]);

        return std::string();
    }

    ++radarLevelIndex;
    if (radarLevelIndex >= radarLevels.size())
        radarLevelIndex = 0;

    if (radarLevels[radarLevelIndex] == 0.0f)
        BZDB.set("displayRadar", "0");
    else
    {
        BZDB.setFloat("displayRadarRange", radarLevels[radarLevelIndex]);
        BZDB.set("displayRadar", "1");
    }

    return std::string();
}

static std::string cmdCyclePanel(const std::string&,
                                 const CommandManager::ArgList& args, bool*)
{
    if (args.size() != 1)
        return "usage: cyclePanel {left[_off]|right[_off]}\n";

    bool forward = args[0] == "right" || args[0] == "right_off";
    bool includeOff = args[0] == "left_off" || args[0] == "right_off";

    if (! BZDB.isTrue("displayConsole"))
    {
        if (forward && includeOff && controlPanel->getMessagesMode() == 3)
            // reversed directions... put it back at beginning
            controlPanel->setMessagesMode(0);
        else if (! forward && includeOff && controlPanel->getMessagesMode() == 0)
            // reversed directions... put it back at end
            controlPanel->setMessagesMode(3);

        BZDB.setBool("displayConsole", true);
    }
    else
    {
        if (controlPanel->getMessagesMode() == 0)
        {
            controlPanel->setMessagesMode(forward ? 1 : 3);

            if (! forward && includeOff)
                BZDB.setBool("displayConsole", false);
        }
        else if (controlPanel->getMessagesMode() == 1)
            controlPanel->setMessagesMode(forward ? 2 : 0);
        else if (controlPanel->getMessagesMode() == 2)
            controlPanel->setMessagesMode(forward ? 3 : 1);
        else
        {
            controlPanel->setMessagesMode(forward ? 0 : 2);

            if (forward && includeOff)
                BZDB.setBool("displayConsole", false);
        }
    }

    return std::string();
}


// Local Variables: ***
// mode: C++ ***
// tab-width: 4 ***
// c-basic-offset: 4 ***
// indent-tabs-mode: nil ***
// End: ***
// ex: shiftwidth=4 tabstop=4
